Frontend Forever App
We have a mobile app for you to download and use. And you can unlock many features in the app.
Get it now
Intall Later
Run
HTML
CSS
Javascript
Output
Jelly Boids
@charset "UTF-8"; @import url(https://fonts.googleapis.com/css?family=Nunito+Sans:300,400,600,700,800); *, :after, :before { box-sizing: border-box; padding: 0; margin: 0; } body { margin: 0; padding: 0; overflow: hidden; }
console.log("Event Fired") // Copyright © Jacob DeHart 2018: http://jacobd.com/ // Inspired by Richard Poole: http://sycora.com/ // Algorithm by Craig Reynolds: http://www.red3d.com/cwr/boids/ // Licence for this file: http://creativecommons.org/licenses/MIT/ function boids() { var canvas = document.createElement('CANVAS'), context = canvas.getContext('2d'), mouse = {down: false}, boidCount = 40, boidMinSpeed = 2, boidMaxSpeed = 4, boidMinSize = 1, boidMaxSize = 1, boidMaxTurn = 10 * Math.PI / 180, boidFOV = 270 * Math.PI / 180, boidCosFOVDiv2 = Math.cos(boidFOV / 2), boidDOF = 80, boidPositionHistoryLength = 6, separationPriority = 15, alignmentPriority = 2, cohesionPriority = 1, targetPriority = 3, mouse = {x: 0, y: 0, down: false}, debug = false, backgroundAlpha = 0.5, allBoids = []; function white(alpha) { return 'RGBA(255, 255, 255, ' + alpha + ')'; } function text(txt, x, y) { context.fillStyle = white(0.9); context.font = '14px Courier'; context.fillText(txt, x, y); } function randBetween(a, b) { return a + Math.random() * (b - a); } function Boid() { var r = Math.floor(randBetween(50, 255)), g = Math.floor(randBetween(50, 255)), b = Math.floor(randBetween(50, 255)); this._color = 'RGBA(' + r + ', ' + g + ', ' + b + ', 1)'; this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.positionHistory = []; this.setDirection(6.28 * Math.random()); this.speed = randBetween(boidMinSpeed, boidMaxSpeed); this.size = randBetween(boidMinSize, boidMaxSize); this.id = allBoids.length; this.first = this.id === 0; allBoids.push(this); } Boid.prototype = { x: 0, y: 0, vx: 0, vy: 0, direction: 0, speed: 0, size: 0, color: function () { return this._color; }, drawCenter: function () { context.fillStyle = white(0.8); context.beginPath(); context.arc(this.x, this.y, this.first ? 5 : 1, 0, 2 * Math.PI); context.fill(); }, drawBody: function () { var i, x, y, offset = 2; if (debug) { context.strokeStyle = white(1); context.beginPath(); context.arc(this.x, this.y, 10 + this.size, 0, 2 * Math.PI); context.stroke(); } else { context.save(); context.strokeStyle = 'RGBA(0, 0, 0, 0.3)'; context.lineWidth = 30; context.lineCap = 'round'; context.beginPath(); context.moveTo(this.x, this.y + offset); x = this.x; y = this.y + offset; for (i = this.positionHistory.length - 1; i >= 0; i -- ) { x -= this.positionHistory[i][0]; y -= this.positionHistory[i][1]; context.lineTo(x, y); } context.stroke(); context.closePath(); context.restore(); context.save(); context.strokeStyle = this.color(); context.lineWidth = 30 - offset; context.lineCap = 'round'; context.beginPath(); context.moveTo(this.x, this.y); x = this.x; y = this.y; for (i = this.positionHistory.length - 1; i >= 0; i -- ) { x -= this.positionHistory[i][0]; y -= this.positionHistory[i][1]; context.lineTo(x, y); } context.stroke(); context.closePath(); context.restore(); context.save(); context.strokeStyle = '#FFF'; context.lineWidth = offset; context.lineCap = 'round'; context.beginPath(); context.moveTo(this.x, this.y - 10); x = this.x; y = this.y - 10; for (i = this.positionHistory.length - 1; i >= 0; i -- ) { x -= this.positionHistory[i][0]; y -= this.positionHistory[i][1]; context.lineTo(x, y); } context.stroke(); context.closePath(); context.restore(); } }, drawDirection: function () { var x = this.x + Math.cos(this.direction) * (this.size * 1), y = this.y + Math.sin(this.direction) * (this.size * 1); context.strokeStyle = white(0.2); context.beginPath(); context.moveTo(this.x, this.y); context.lineTo(x, y); context.stroke(); context.fillStyle = white(0.8); context.beginPath(); context.arc(x, y, 1, 0, 2 * Math.PI); context.fill(); }, drawTarget: function () { var x = this.x + Math.cos(this.direction) * (this.size * 1), y = this.y + Math.sin(this.direction) * (this.size * 1); context.strokeStyle = white(1 / boidCount); context.beginPath(); context.moveTo(x, y); context.lineTo(mouse.x, mouse.y); context.stroke(); }, drawVision() { var size = this.size + boidDOF, leftAngle = this.direction - boidFOV / 2, rightAngle = this.direction + boidFOV / 2; context.strokeStyle = white(0.2); context.beginPath(); context.moveTo(this.x, this.y); context.arc(this.x, this.y, size, leftAngle, rightAngle); context.lineTo(this.x, this.y); context.stroke(); }, draw: function () { this.drawBody(); if (debug) { this.drawCenter(); this.drawDirection(); this.drawTarget(); this.drawVision(); } }, move: function () { var dx = this.vx * this.speed, dy = this.vy * this.speed; this.x += dx; this.y += dy; this.positionHistory.push([dx, dy]); if (this.positionHistory.length > boidPositionHistoryLength) { this.positionHistory.shift(); } }, setDirection: function (v) { this.direction = v; this.vx = Math.cos(this.direction); this.vy = Math.sin(this.direction); }, analyzeNeighbors: function () { var neighborData = { x: 0, y: 0, vx: 0, vy: 0, closestDistance: 999999999, count: 0 }; allBoids.forEach(function (boid, i) { if (boid === this) { return; } var dx = boid.x - this.x, dy = boid.y - this.y, distanceSq = dx * dx + dy * dy, distance = Math.sqrt(distanceSq) - this.size - boid.size, dxPercent = dx / (distance + this.size + boid.size), dyPercent = dy / (distance + this.size + boid.size), cosAngle = boid.vx * dxPercent + boid.vy * dyPercent; if (distance < neighborData.closestDistance) { neighborData.closestDistance = distance; neighborData.closestBoid = boid; } if (distance <= boidDOF + this.size && cosAngle >= boidCosFOVDiv2) { neighborData.x += boid.x; neighborData.y += boid.y; neighborData.vx += boid.vx; neighborData.vy += boid.vy; neighborData.count += 1; } }.bind(this)); neighborData.x /= neighborData.count; neighborData.y /= neighborData.count; neighborData.direction = Math.atan2(neighborData.vy, neighborData.vx); this.neighborData = neighborData; }, applySeparation: function () { var dx, dy, nd = this.neighborData; if (nd.closestBoid && nd.closestDistance < boidDOF + this.size - nd.closestBoid.size) { dx = nd.closestBoid.x - this.x; dy = nd.closestBoid.y - this.y; this.targetAngleX -= dx / nd.closestDistance * separationPriority; this.targetAngleY -= dy / nd.closestDistance * separationPriority; } }, applyAlignment: function () { var nd = this.neighborData; this.targetAngleX += Math.cos(nd.direction) * alignmentPriority; this.targetAngleY += Math.sin(nd.direction) * alignmentPriority; }, applyCohesion: function () { var dx, dy, dxPercent, dyPercent, distance, nd = this.neighborData; if (nd.count > 0) { dx = nd.x - this.x; dy = nd.y - this.y; distance = Math.sqrt(dx * dx + dy * dy); dxPercent = dx / distance; dyPercent = dy / distance; this.targetAngleX += dxPercent * cohesionPriority; this.targetAngleY += dyPercent * cohesionPriority; } }, applyPrimaryTarget: function () { var dx = mouse.x - this.x, dy = mouse.y - this.y, distance = Math.max(Math.sqrt(dx * dx + dy * dy), 50), dxPercent = dx / distance, dyPercent = dy / distance; this.targetAngleX += dxPercent * targetPriority; this.targetAngleY += dyPercent * targetPriority; }, initTargetAngle: function () { this.targetAngleX = this.vx; this.targetAngleY = this.vy; }, calcTargetAngle: function () { this.targetAngle = Math.atan2(this.targetAngleY, this.targetAngleX); }, fixTargetAngle: function () { var deltaAngle = (this.targetAngle - this.direction + Math.PI * 4) % (Math.PI * 2), aDeltaAngle = (this.direction - this.targetAngle + Math.PI * 4) % (Math.PI * 2), delta = Math.abs(deltaAngle) < Math.abs(aDeltaAngle) ? deltaAngle : -aDeltaAngle; this.targetAngle = delta; }, applyMaxTurn: function () { this.targetAngle = Math.min(Math.max(this.targetAngle, -boidMaxTurn), boidMaxTurn); }, applyTargetAngle() { this.setDirection(this.direction + this.targetAngle); }, updateDirection: function () { this.initTargetAngle(); this.analyzeNeighbors(); this.applySeparation(); this.applyAlignment(); this.applyCohesion() this.applyPrimaryTarget(); this.calcTargetAngle(); this.fixTargetAngle(); this.applyMaxTurn(); this.applyTargetAngle(); }, wrap: function () { if (this.x > canvas.width + this.size / 2) { this.x -= canvas.width + this.size; } else if (this.x < 0 - this.size / 2) { this.x += canvas.width + this.size; } if (this.y > canvas.height + this.size / 2) { this.y -= canvas.height + this.size; } else if (this.y < 0 - this.size / 2) { this.y += canvas.height + this.size; } }, update: function () { this.move(); this.updateDirection(); this.wrap(); this.draw(); } }; function updateWindow() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } function handleMove(e) { e.preventDefault(); mouse.x = e.pageX; mouse.y = e.pageY; backgroundAlpha = 0.3; } window.onmousedown = window.ontouchstart = function () { mouse.down = true; debug = !debug; }; window.onmouseup = window.ontouchend = function () { mouse.down = false; }; window.onresize = window.orientationchange = updateWindow; canvas.onmousemove = canvas.ontouchmove = handleMove; function renderLoop() { requestAnimationFrame(renderLoop); context.globalAlpha = 1; if (debug) { context.fillStyle = '#043F8C'; } else { // context.fillStyle = 'RGBA(50, 50, 50, ' + 1 + ')'; // context.fillStyle = 'RGBA(255, 255, 255, ' + 1 + ')'; context.fillStyle = '#c70017'; } context.fillRect(0, 0, canvas.width, canvas.height); context.globalAlpha = 1; backgroundAlpha += (1 - backgroundAlpha) / 100; if (debug) { text('Click to toggle debug', 10, 20); context.strokeStyle = white(0.3); context.beginPath(); context.arc(mouse.x, mouse.y, 50, 0, 2 * Math.PI); context.stroke(); } allBoids.forEach(function (boid) { boid.update(); }); } document.body.appendChild(canvas); updateWindow(); mouse.x = canvas.width / 2; mouse.y = canvas.height / 2; for (var i = 0; i < boidCount; i++) { new Boid(); } renderLoop(); } window.onload = boids;